import * as THREE from 'three'

import { GUI } from 'three/addons/libs/lil-gui.module.min.js'

import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js'
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js'
import { OutlinePass } from 'three/addons/postprocessing/OutlinePass.js'
import { FXAAShader } from 'three/addons/shaders/FXAAShader.js'

const BLOOM_SCENE = 1

const bloomLayer = new THREE.Layers()
bloomLayer.set(BLOOM_SCENE)

const params = {
  threshold: 0,
  strength: 1,
  radius: 0.5,
  exposure: 1,
}

const darkMaterial = new THREE.MeshBasicMaterial({ color: 'black' })
const materials = {}

const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.toneMapping = THREE.ReinhardToneMapping
document.body.appendChild(renderer.domElement)

const scene = new THREE.Scene()

const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 1, 200)
camera.position.set(0, 0, 20)
camera.lookAt(0, 0, 0)

const controls = new OrbitControls(camera, renderer.domElement)
controls.maxPolarAngle = Math.PI * 0.5
controls.minDistance = 1
controls.maxDistance = 100
controls.addEventListener('change', render)

const renderScene = new RenderPass(scene, camera)

const bloomPass = new UnrealBloomPass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  1.5,
  0.4,
  0.85
)
bloomPass.threshold = params.threshold
bloomPass.strength = params.strength
bloomPass.radius = params.radius

const bloomComposer = new EffectComposer(renderer)
bloomComposer.renderToScreen = false
bloomComposer.addPass(renderScene)
bloomComposer.addPass(bloomPass)

const mixPass = new ShaderPass(
  new THREE.ShaderMaterial({
    uniforms: {
      baseTexture: { value: null },
      bloomTexture: { value: bloomComposer.renderTarget2.texture },
    },
    vertexShader: /* glsl */ `
      varying vec2 vUv;

      void main() {

        vUv = uv;

        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );

      }
    `,
    fragmentShader: /* glsl */ `
      uniform sampler2D baseTexture;
      uniform sampler2D bloomTexture;

      varying vec2 vUv;

      void main() {

        gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 1.0 ) * texture2D( bloomTexture, vUv ) );

      }
    `,
    defines: {},
  }),
  'baseTexture'
)
mixPass.needsSwap = true

const outlinePass = new OutlinePass(
  new THREE.Vector2(window.innerWidth, window.innerHeight),
  scene,
  camera
)

const outputPass = new OutputPass()

const fxaaPass = new ShaderPass(FXAAShader)
fxaaPass.uniforms['resolution'].value.set(
  1 / window.innerWidth / window.devicePixelRatio,
  1 / window.innerHeight / window.devicePixelRatio,
)


const finalComposer = new EffectComposer(renderer)
finalComposer.addPass(renderScene)
finalComposer.addPass(mixPass)
finalComposer.addPass(outlinePass)
finalComposer.addPass(outputPass)
finalComposer.addPass(fxaaPass)

const raycaster = new THREE.Raycaster()

const mouse = new THREE.Vector2()

window.addEventListener('pointerdown', onPointerDown)

const gui = new GUI()

const bloomFolder = gui.addFolder('bloom')

bloomFolder.add(params, 'threshold', 0.0, 1.0).onChange(function (value) {
  bloomPass.threshold = Number(value)
  render()
})

bloomFolder.add(params, 'strength', 0.0, 3).onChange(function (value) {
  bloomPass.strength = Number(value)
  render()
})

bloomFolder
  .add(params, 'radius', 0.0, 1.0)
  .step(0.01)
  .onChange(function (value) {
    bloomPass.radius = Number(value)
    render()
  })

const toneMappingFolder = gui.addFolder('tone mapping')

toneMappingFolder.add(params, 'exposure', 0.1, 2).onChange(function (value) {
  renderer.toneMappingExposure = Math.pow(value, 4.0)
  render()
})

setupScene()

function onPointerDown(event) {
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1

  raycaster.setFromCamera(mouse, camera)
  const intersects = raycaster.intersectObjects(scene.children, false)
  if (intersects.length > 0) {
    const object = intersects[0].object
    object.layers.toggle(BLOOM_SCENE)
    outlinePass.selectedObjects.includes(object)
      ? outlinePass.selectedObjects.splice(outlinePass.selectedObjects.indexOf(object), 1)
      : outlinePass.selectedObjects.push(object)
    render()
  }
}

window.onresize = function () {
  const width = window.innerWidth
  const height = window.innerHeight

  camera.aspect = width / height
  camera.updateProjectionMatrix()

  renderer.setSize(width, height)

  bloomComposer.setSize(width, height)
  finalComposer.setSize(width, height)

  render()
}

function setupScene() {
  scene.traverse(disposeMaterial)
  scene.children.length = 0

  const geometry = new THREE.IcosahedronGeometry(1, 15)

  for (let i = 0; i < 50; i++) {
    const color = new THREE.Color()
    color.setHSL(Math.random(), 0.7, Math.random() * 0.2 + 0.05)

    const material = new THREE.MeshBasicMaterial({ color: color })
    const sphere = new THREE.Mesh(geometry, material)
    sphere.position.x = Math.random() * 10 - 5
    sphere.position.y = Math.random() * 10 - 5
    sphere.position.z = Math.random() * 10 - 5
    sphere.position.normalize().multiplyScalar(Math.random() * 4.0 + 2.0)
    sphere.scale.setScalar(Math.random() * Math.random() + 0.5)
    scene.add(sphere)

    if (Math.random() < 0.25) {
      sphere.layers.enable(BLOOM_SCENE)
      outlinePass.selectedObjects.push(sphere)
    }
  }

  render()
}

function disposeMaterial(obj) {
  if (obj.material) {
    obj.material.dispose()
  }
}

function render() {
  scene.traverse(darkenNonBloomed)
  bloomComposer.render()
  scene.traverse(restoreMaterial)

  // render the entire scene, then render bloom scene on top
  finalComposer.render()
}

function darkenNonBloomed(obj) {
  if (obj.isMesh && bloomLayer.test(obj.layers) === false) {
    materials[obj.uuid] = obj.material
    obj.material = darkMaterial
  }
}

function restoreMaterial(obj) {
  if (materials[obj.uuid]) {
    obj.material = materials[obj.uuid]
    delete materials[obj.uuid]
  }
}
